﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using Roslyn.Compilers.CSharp;

namespace SharpKit.Rx.Generator
{
	static class Extensions
	{
		public static IEnumerable<T> WithFirst<T>(this IEnumerable<T> source, T value)
		{
			return Enumerable.Repeat(value, 1).Concat(source);
		}

		public static IEnumerable<T> WithLast<T>(this IEnumerable<T> source, T value)
		{
			return source.Concat(Enumerable.Repeat(value, 1));
		}

		public static SyntaxList<T> ToSyntaxList<T>(this IEnumerable<T> source) where T : SyntaxNode
		{
			return Syntax.List(source);
		}

		public static SyntaxList<T> WithFirst<T>(this SyntaxList<T> source, T value) where T : SyntaxNode
		{
			return Enumerable.Repeat(value, 1).Concat(source).ToSyntaxList();
		}

		public static SyntaxList<T> WithLast<T>(this SyntaxList<T> source, T value) where T : SyntaxNode
		{
			return source.Concat(Enumerable.Repeat(value, 1)).ToSyntaxList();
		}

		public static IEnumerable<SyntaxNodeOrToken> ToSeparated<T>(this IEnumerable<T> source, SyntaxToken separator)
			where T: SyntaxNode
		{
			var first = true;
			foreach (var value in  source)
			{
				if (first)
					first = false;
				else
					yield return separator;
				yield return value;
			}
		}

		public static SeparatedSyntaxList<T> ToSeparatedSyntaxList<T>(this IEnumerable<T> source, SyntaxToken separator) where T : SyntaxNode
		{
			return Syntax.SeparatedList<T>(source.ToSeparated(separator));
		}

		public static SeparatedSyntaxList<T> ToSeparatedSyntaxList<T>(this IEnumerable<T> source) where T : SyntaxNode
		{
			return Syntax.SeparatedList<T>(source.ToSeparated(Syntax.Token(SyntaxKind.CommaToken)));
		}

		public static SeparatedSyntaxList<T> WithFirst<T>(this SeparatedSyntaxList<T> source, T value) where T : SyntaxNode
		{
			return Enumerable.Repeat(value, 1).Concat(source).ToSeparatedSyntaxList();
		}

		public static SeparatedSyntaxList<T> WithLast<T>(this SeparatedSyntaxList<T> source, T value) where T : SyntaxNode
		{
			return source.Concat(Enumerable.Repeat(value, 1)).ToSeparatedSyntaxList();
		}
	}

	class Transformer: SyntaxRewriter
	{
		private static readonly QualifiedNameSyntax JsNs = Syntax.QualifiedName(left: Syntax.IdentifierName("SharpKit"), right: Syntax.IdentifierName("JavaScript"));
		private static readonly QualifiedNameSyntax JsEnumName = Syntax.QualifiedName(left: JsNs, right: Syntax.IdentifierName("JsEnum"));
		private static readonly QualifiedNameSyntax JsTypeName = Syntax.QualifiedName(left: JsNs, right: Syntax.IdentifierName("JsType"));
		private static readonly QualifiedNameSyntax JsMethodName = Syntax.QualifiedName(left: JsNs, right: Syntax.IdentifierName("JsMethod"));

		private static readonly AttributeDeclarationSyntax JsEnumAttribute = Syntax.AttributeDeclaration(
			attributes: Syntax.SeparatedList(Syntax.Attribute(
				name: JsEnumName,
				argumentListOpt: Syntax.AttributeArgumentList(
					arguments: Syntax.SeparatedList(Syntax.AttributeArgument(
						nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("ValuesAsNames")),
						expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)))))));

		private static readonly AttributeDeclarationSyntax JsMethodAttribute = Syntax.AttributeDeclaration(
			attributes: Syntax.SeparatedList(Syntax.Attribute(
				name: JsMethodName,
				argumentListOpt: Syntax.AttributeArgumentList(
					arguments: Syntax.SeparatedList(Syntax.AttributeArgument(
						nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("ExtensionImplementedInInstance")),
						expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)))))));

		private static readonly AttributeSyntax JsTypeAttribute = Syntax.Attribute(
			name: JsTypeName,
			argumentListOpt: Syntax.AttributeArgumentList(
				arguments: new[]
				           {
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("Mode")),
				           		expression: Syntax.ParseExpression("SharpKit.JavaScript.JsMode.Prototype")),
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("Export")),
				           		expression: Syntax.LiteralExpression(SyntaxKind.FalseLiteralExpression)),
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("IgnoreGenericMethodArguments")),
				           		expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)),
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("AutomaticPropertiesAsFields")),
				           		expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)),
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("IgnoreGenericTypeArguments")),
				           		expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)),
				           	Syntax.AttributeArgument(
				           		nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("PropertiesAsFields")),
				           		expression: Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression))
				           }.ToSeparatedSyntaxList()
				));

		protected override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node)
		{
			if (!node.Modifiers.Any(SyntaxKind.PublicKeyword))
				return base.VisitEnumDeclaration(node);
			if (node.Attributes.Any(a => a.Attributes.Any(aa => aa.Name.PlainName == "JsEnum")))
				return base.VisitEnumDeclaration(node);

			return node.Update(
				attributes: Syntax.List(node.Attributes.Concat(Enumerable.Repeat(JsEnumAttribute.WithLeadingTrivia(Syntax.ParseLeadingTrivia("\t")).WithTrailingTrivia(Syntax.ParseTrailingTrivia("\r\n")), 1))),
				modifiers: node.Modifiers,
				enumKeyword: node.EnumKeyword,
				identifier: node.Identifier,
				baseListOpt: node.BaseListOpt,
				openBraceToken: node.OpenBraceToken,
				members: node.Members,
				closeBraceToken: node.CloseBraceToken,
				semicolonTokenOpt: node.SemicolonTokenOpt);
		}

		private static AttributeSyntax CreateJsTypeAttribute(string name)
		{
			return Syntax.Attribute(
				name: JsTypeAttribute.Name,
				argumentListOpt: Syntax.AttributeArgumentList(
					arguments: JsTypeAttribute.ArgumentListOpt.Arguments.WithFirst(Syntax.AttributeArgument(
						nameEqualsOpt: Syntax.NameEquals(Syntax.Identifier("Name")),
						expression: Syntax.ParseExpression("\"Rx." + name + "\"")))));
		}

		protected override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
		{
			if (!node.Modifiers.Any(SyntaxKind.PublicKeyword))
				return base.VisitClassDeclaration(node);
			if (node.Attributes.Any(a => a.Attributes.Any(aa => aa.Name.PlainName == "JsType")))
				return base.VisitClassDeclaration(node);

			node = node.Update(
				attributes: node.Attributes.WithLast(Syntax.AttributeDeclaration(
					attributes: Syntax.SeparatedList(CreateJsTypeAttribute(node.Identifier.ValueText)))
					.WithLeadingTrivia(Syntax.ParseLeadingTrivia("\t")).WithTrailingTrivia(Syntax.ParseTrailingTrivia("\r\n"))),
				modifiers: node.Modifiers,
				keyword: node.Keyword,
				identifier: node.Identifier,
				typeParameterListOpt: node.TypeParameterListOpt,
				baseListOpt: node.BaseListOpt,
				constraintClauses: node.ConstraintClauses,
				openBraceToken: node.OpenBraceToken,
				members: node.Members,
				closeBraceToken: node.CloseBraceToken,
				semicolonTokenOpt: node.SemicolonTokenOpt);

			return base.VisitClassDeclaration(node);
		}

		protected override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
		{
			if (!node.Modifiers.Any(SyntaxKind.PublicKeyword))
				return base.VisitInterfaceDeclaration(node);
			if (node.Attributes.Any(a => a.Attributes.Any(aa => aa.Name.PlainName == "JsType")))
				return base.VisitInterfaceDeclaration(node);

			node = node.Update(
				attributes: node.Attributes.WithLast(Syntax.AttributeDeclaration(
					attributes: Syntax.SeparatedList(CreateJsTypeAttribute(node.Identifier.ValueText)))
					.WithLeadingTrivia(Syntax.ParseLeadingTrivia("\t")).WithTrailingTrivia(Syntax.ParseTrailingTrivia("\r\n"))),
				modifiers: node.Modifiers,
				keyword: node.Keyword,
				identifier: node.Identifier,
				typeParameterListOpt: node.TypeParameterListOpt,
				baseListOpt: node.BaseListOpt,
				constraintClauses: node.ConstraintClauses,
				openBraceToken: node.OpenBraceToken,
				members: node.Members,
				closeBraceToken: node.CloseBraceToken,
				semicolonTokenOpt: node.SemicolonTokenOpt);

			return base.VisitInterfaceDeclaration(node);
		}

		protected override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
		{
			if (!node.Modifiers.Any(SyntaxKind.PublicKeyword))
				return base.VisitMethodDeclaration(node);
			if (node.Attributes.Any(a => a.Attributes.Any(aa => aa.Name.PlainName == "JsMethod")))
				return base.VisitMethodDeclaration(node);

			return node.Update(
				attributes: node.Attributes.WithLast(JsMethodAttribute.WithLeadingTrivia(Syntax.ParseLeadingTrivia("\t\t")).WithTrailingTrivia(Syntax.ParseTrailingTrivia("\r\n"))),
				modifiers: node.Modifiers,
				returnType: node.ReturnType,
				explicitInterfaceSpecifierOpt: node.ExplicitInterfaceSpecifierOpt,
				identifier: node.Identifier,
				typeParameterListOpt: node.TypeParameterListOpt,
				parameterList: node.ParameterList,
				constraintClauses: node.ConstraintClauses,
				bodyOpt: node.BodyOpt,
				semicolonTokenOpt: node.SemicolonTokenOpt);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			const string sourcePath = "..\\..\\..\\SharpKit.Rx.Source\\";
			const string targetPath = "..\\..\\..\\SharpKit.Rx\\";
			var files = Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, sourcePath), "*.cs").ToList();

			Console.WriteLine("Enumerated {0} files", files.Count);

			var rewriter = new Transformer();

			foreach (var file in files)
			{
				var filename = Path.GetFileNameWithoutExtension(file);
				Console.WriteLine("Rewriting {0}.cs -> {0}.generated.cs...", filename);
				var sourceCode = File.ReadAllText(file);
				var syntaxTree = SyntaxTree.ParseCompilationUnit(sourceCode).Root;

				syntaxTree = rewriter.Visit(syntaxTree);

				File.WriteAllText(Path.Combine(Environment.CurrentDirectory, targetPath, filename + ".generated.cs"), syntaxTree.GetText());
			}
		}
	}
}
